package middleware

import (
	"errors"
	"ginvueadmin/global"
	"ginvueadmin/models"
	"ginvueadmin/models/request"
	"ginvueadmin/service"
	"strconv"
	"time"

	"github.com/dgrijalva/jwt-go"
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
)

var (
	ErrTokenExpired     = errors.New("token is expired")
	ErrTokenNotValidYet = errors.New("token not active yet")
	ErrTokenMalformed   = errors.New("that's not even a token")
	ErrTokenInvalid     = errors.New("couldn't handle this token")
)

type JWT struct {
	SigningKey []byte
}

func NewJwt() *JWT {
	return &JWT{
		[]byte(global.CONFIG.Jwt.SigningKey),
	}
}

// 创建 token
func (j *JWT) CreateToken(claims request.CustomClaims) (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	return token.SignedString(j.SigningKey)
}

// 解析 token
func (j *JWT) ParseToken(tokenString string) (*request.CustomClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &request.CustomClaims{}, func(token *jwt.Token) (i interface{}, e error) {
		return j.SigningKey, nil
	})

	// 处理 err 异常的各种场景
	if err != nil {
		if ve, ok := err.(*jwt.ValidationError); ok {
			if ve.Errors&jwt.ValidationErrorMalformed != 0 {
				return nil, ErrTokenMalformed
			} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
				// Token is Errexpired
				return nil, ErrTokenExpired
			} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
				return nil, ErrTokenNotValidYet
			} else if ve.Errors&jwt.ValidationErrorUnverifiable != 0 {
				return nil, ErrTokenInvalid
			}
		}
	}

	if token != nil {
		if claims, ok := token.Claims.(*request.CustomClaims); ok && token.Valid {
			return claims, nil
		}

		return nil, ErrTokenInvalid
	} else {
		return nil, ErrTokenInvalid
	}
}

// 进行 JWT 认证
func JWTAuth() gin.HandlerFunc {
	// jwt 鉴权取头部信息 x-token 登录时回返回 token 信息
	// 这里前端需要把 token 存储到 cookie 或者本地 localSstorage 中，不过需要跟后端协商过期时间，可以约定刷新令牌或者重新登录
	return func(c *gin.Context) {
		/*
			1. 从请求头 c.Request.Header 中获取 x-token 字段作为 JWT 的令牌。
			2. 如果令牌为空，则返回未登录或非法访问的错误响应，并终止请求 c.Abort()。
			3. 如果令牌存在于黑名单 service.IsBlackList() 中，则返回帐户异地登录或令牌失效的错误响应，并终止请求 c.Abort()。
			4. 创建一个新的 JWT 实例。
			5. 使用令牌解析方法 ParseToken 解析令牌，获取其中包含的信息。
				a. 如果解析过程异常，根据不同的错误类型返回相应的错误响应，并终止请求Err。
					i. ErrTokenExpired 的错误，Err提示授权已过期
			6. 如果令牌即将过期，更新令牌的过期时间，并生成新的令牌。将新的令牌和过期时间添加到响应头。
				a. 通过 claims 的记录的 ExpiresAt 与当前时间差值跟 clais.BufferTime 比较，若 BufferTime 更大，则更新 token
					i. 重新生成 ExpiresAt 时间参数，调用 CreateToken 重新生成 token
					ii. 并且解析新生成的 token，记录新的 newClaims
					iii. 把 i 与 ii 处理的 newToken 与 newClaims 的值添加到响应头中 c.Header('new-token'|'new-expires-at')
				b. 如果系统配置中启用了多点登录（UseMultipoint），则检查令牌对应的用户名是否在 Redis 中存在旧的令牌。如果存在，则将旧令牌添加到黑名单。
					i. 首先，通过调用 GetRedisJWT 方法从 Redis 中获取与当前令牌对应的用户名的旧令牌（如果存在的话）。
					ii. 如果获取到了旧令牌，说明当前用户在其他地方已经登录，为了保证安全性，需要将旧令牌添加到黑名单中 service.JsonInBlacklist()，使其失效。
					iii. 无论是否成功获取到旧令牌，都需要将当前令牌和用户名存储到 Redis 中 service.SetRedisJWT()，表示当前用户的活跃状态。
			7. 将解析后的令牌中的信息设置到 Gin 的上下文中，以便后续处理使用。
				i. c.Set('claims', claims)
			8. 继续处理后续的请求逻辑。
				i. c.Next()
		*/

		token := c.Request.Header.Get("x-token")
		if token == "" {
			global.Result(global.ERROR, 401, "未登录或非法访问", c)
			c.Abort()
			return
		}

		// 如果令牌存在于黑名单 service.IsBlackList() 中，则返回帐户异地登录或令牌失效的错误响应，并终止请求 c.Abort()。
		if service.IsBlackList(token) {
			global.Result(global.ERROR, 401, "帐户异地登录或令牌失效", c)

			c.Abort()
			return
		}

		j := NewJwt()
		claims, err := j.ParseToken(token)
		if err != nil {
			if err == ErrTokenExpired {
				global.Result(global.ERROR, 401, "令牌已过期", c)
				c.Abort()
				return
			}

			global.Result(global.ERROR, 401, "非法访问", c)
			c.Abort()
			return
		}

		// 如果令牌即将过期，更新令牌的过期时间，并生成新的令牌。将新的令牌和过期时间添加到响应头。
		// a. 通过 claims 的记录的 ExpiresAt 与当前时间差值跟 clais.BufferTime 比较，若 BufferTime 更大，则更新 token
		// 			i. 重新生成 ExpiresAt 时间参数，调用 CreateToken 重新生成 token
		// 			ii. 并且解析新生成的 token，记录新的 newClaims
		// 			iii. 把 i 与 ii 处理的 newToken 与 newClaims 的值添加到响应头中 c.Header('new-token'|'new-expires-at')
		// 		b. 如果系统配置中启用了多点登录（UseMultipoint），则检查令牌对应的用户名是否在 Redis 中存在旧的令牌。如果存在，则将旧令牌添加到黑名单。
		// 			i. 首先，通过调用 GetRedisJWT 方法从 Redis 中获取与当前令牌对应的用户名的旧令牌（如果存在的话）。
		// 			ii. 如果获取到了旧令牌，说明当前用户在其他地方已经登录，为了保证安全性，需要将旧令牌添加到黑名单中 service.JsonInBlacklist()，使其失效。
		// 			iii. 无论是否成功获取到旧令牌，都需要将当前令牌和用户名存储到 Redis 中 service.SetRedisJWT()，表示当前用户的活跃状态。
		if claims.ExpiresAt-time.Now().Unix() < claims.BufferTime {
			claims.ExpiresAt = time.Now().Unix() + 60*60*24*7
			newToken, _ := j.CreateToken(*claims)
			newClaims, _ := j.ParseToken(newToken)

			c.Header("new-token", newToken)
			c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt, 10))

			// 多点登录
			if global.CONFIG.System.UseMultipoint {
				redisJwt, err := service.GetRedisJwt(newClaims.Username)
				if err != nil {
					global.LOG.Error("多点登录，获取旧令牌失败", zap.Any("err", err))
				} else {
					service.JsonInBlacklist(models.JwtBlacklist{Jwt: redisJwt})
				}

				// 必须记录当前活跃状态
				_ = service.SetRedisJwt(newClaims.Username, newToken)
			}
		}

		// 设置回 gin 中
		c.Set("claims", claims)
		c.Next()
	}
}
